Introduction
Back in April 2008, I posted my first .Net article here on CodeProject:
Anagrams - A Word Game in C#[^]
It's a simple word game that allowed the user to find anagrams within a scrambled word, awarding Scrabble-based points and extra time based on the word found. I reveled in my cleverocity (I know - that's not really a word, which is ironic when you consider the nature of the code this article describes), and congratulated myself for a job well-done. Since my current job requires coding within WPF, I got the urge to revisit Anagrams, and give it a much-needed facelift.
I must admit that while I tried to use certain patterns and WPF practices, I certainly didn't make heroic efforts to stay rigidly within their implied guidelines. Everything in programming is a trade-off, and rigid application of those kinds of rules has absolutely no place in software development. Given the nature of and size of this application, that idea is even more applicable.
Featured Technologies
The following technologies and code elements are present in this application:
- Visual Studio 12
- WPF
- MVVM pattern
- Dispatch Timers
- Data Binding
- Value Converters
- Linq
- Bacon
IMPORTANT NOTICE! *I THINK* you have to install .Net 4.5 before running this application, or change the set method on the
GameDictionary.PercentRemaining
property to
public
by removing the
private
accessor).
General Architecture
This version of the game contains a lot less code than the original. I'm not sure if that's due mostly to a combination of my increased knowledge of .Net (combined with its latest features), or due to the use MVVM/WPF. Whatever the cause, it's a good thing.
Words are loaded from a text file on disk (in the old version there was a text file for each word size), and each time user starts a game, a list of words is created for use by the interface. This list of words contains ALL of the words that can be found within the currently selected game word. This pattern is probably the biggest reason there is less code in the application. In the old version, all words were available to the game all the time, and I had to maintain several indicators, indexes, and other crap because I (falsely) assumed that .Net's performance was lacking, and I "coded around" that falsely perceived problem.
The Model
The model is driven by a single comma-delimited text file that contains all of the words that can be used in the game (currently, the word count is over 125,000). When the program runs, the data file is loaded, and points are calculated for each word. A word is represented by the AWord
class:
public class AWord
{
public string Text { get; private set; }
public int Points { get; private set; }
public bool Used { get; protected set; }
public AWord(string text)
{
this.Text = text;
this.Used = false;
this.Points = Globals.CalcWordScore(this.Text);
}
public AWord(AWord word)
{
this.Text = word.Text;
this.Used = word.Used;
this.Points = word.Points;
}
}
In order to have a lower impact on memory and increase the performance, I included the Points
and Used
properties in the model. The primary reason is that the words used in a given game are extracted from the model only when they're needed by the current game. I didn't want to have to recalculate word points for every game (although there are rarely any instances where you'd get more than 300-400 possible words in a given scramble). The Used
property is required to keep track of words used as game words in the current app session, and the only way to maintain tracking is to put this property into the model. What this should tell you is that MVVM is only a guideline, and not a rigid set of requirements. Sometimes, you gotta bend the rules to make the code and data workable within the context of the application in which it is used.
And the list of words is represented by the MainDictionaryClass:
public class MainDictionary : List<AWord>
{
public bool CanPlay { get; set; }
public int ShortestWordSize { get; private set; }
public int LongestWordSize { get; private set; }
public MainDictionary()
{
this.ShortestWordSize = 65535;
this.LongestWordSize = 0;
this.CanPlay = LoadFile(Path.Combine
(Path.GetDirectoryName
(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName),
"Anagrams2Words.txt"));
}
protected bool LoadFile(string fileName)
{
bool success = false;
try
{
using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (StreamReader reader = new StreamReader(stream))
{
string words;
while (!reader.EndOfStream)
{
words = reader.ReadLine();
if (words.Length > 0)
{
string[] wordsplit = words.Split(' ');
for (int i = 0; i < wordsplit.Length; i++)
{
string text = wordsplit[i].ToUpper();
ShortestWordSize = Math.Min(ShortestWordSize, text.Length);
LongestWordSize = Math.Max(LongestWordSize, text.Length);
AWord item = new AWord(text);
Add(item);
}
}
}
Globals.LongestWord = LongestWordSize;
Globals.ShortestWord = ShortestWordSize;
success = this.Count > 0;
}
}
}
catch (Exception e)
{
if (e != null) { }
}
return success;
}
public List<AWord> GetWordsByLength(int length)
{
var list = (from item in this
where item.Text.Length == length
select item).ToList<AWord>();
return list;
}
}
As you can see, there's really not much going on here. The user can't change the list of words from within the program, so there's no reason to be able to save them back to the hard drive. Other than loading the words and calculating their point values, what else is there to do/say?
The View Model
The view model is where most of the work is done during game play. To keep the article size to a reasonable minimum, I didn't include comments that actually exist in the code. Generally speaking, I uses the standard WPF objects to allow the interface to reflect changes in the view model, namely, INotifyPropertyChanged
and ObservableCollection
. Given the amount of documentation available on the internet for these objects, and the fact that I'm not doing anything beyond normal usage, this is the only thing I'm going to say about these objects.
The DisplayWord Class
This class represents a word in the current game, and is created for each word used in the current game. It is derived from AWord
. The properties are as follows:
- string Foreground - This is the foreground color to be used when displaying the word in the
ListBox
on the main window.
- string Scramble - This is the scrambled version of the word. This string will be empty unless this is the current game word.
- bool Found - This property indicates whether or not the user has found this word during game play. When this property is true, the color of the word changes to either blue (a normal found word), or red (if this word is the original word).
- bool IsOriginalWord - This property indicates that this word is the current game word.
The only code of any real interest in this class is the ScrambleIt
method. It's responsible (as you might guess) for scrambling the word.
public void ScrambleIt()
{
StringBuilder scramble = new StringBuilder();
do
{
string temp = this.Text;
do
{
if (temp.Length > 1)
{
int index = (temp.Length > 1) ? Globals.RandomNumber(0, temp.Length-1) : 0;
scramble.Append(temp[index]);
temp = temp.Remove(index, 1);
}
else
{
scramble.Append(temp);
temp = "";
}
} while (!string.IsNullOrEmpty(temp));
} while (scramble.ToString() == this.Text);
this.Scramble = scramble.ToString();
this.IsOriginalWord = true;
}
Since there's a remote possibility that the resulting scrambled word could end up being the original word, the word is re-scrambled until it is not the same as the original word.
The GameStatistics Class
This class maintains statistics relevant to the current game-in-progress, such as how many words of a certain length have been found, how many points have been earned and similar data. there's a little math happening here, but nothing worth further note. I was tempted to include my current warp anti-gravity calculations here, but I didn't want to confuse anyone, or upset those who think it's not possible).
The Settings Class
This class represents the settings that the user can change in the Setup window, and is a view model on those properties. It contains nothing but set/get and a Save
method. Nothing fancy, and certainly not worth discussing any further.
The WordCounts Class
This class represents a list of WordCounItem
objects, and is used within (and exposed from) the GameStatistics
class. A WordCountItem
contains two properties:
- LetterCount - The size of the words being tracked by this item
- WordCount - How many words of this size have been found in the current game
The GameDictionary Class
This class manages game play and is created for each game, which relieves us of having to reset properties in words and the statistics between games. To start things off, we need to determine how many letters the game word will have. This is determined by the game settings, and the default is that a random number of letters will be used
private int SetLetterCount()
{
int letterCount = 0;
int shortest = Math.Max(Globals.MainDictionary.ShortestWordSize, 6);
int longest = Globals.MainDictionary.LongestWordSize;
switch (this.Settings.LetterPoolMode)
{
case LetterPoolMode.Random :
letterCount = Globals.RandomNumber(shortest, longest);
break;
case LetterPoolMode.Static :
letterCount = Settings.LetterPoolCount;
break;
}
return letterCount;
}
Now that we have a word size, we can choose a word at random from our main dictionary.
private AWord SelectNewWord(int count)
{
AWord selectedWord = null;
List<AWord> words = Globals.MainDictionary.GetWordsByLength(count);
if (this.Settings.TrackUsedWords)
{
if (words.Count == 0)
{
words.Clear();
if (this.Settings.LetterPoolMode == LetterPoolMode.Random)
{
count = SetLetterCount();
selectedWord = SelectNewWord(count);
}
else
{
words = Globals.MainDictionary.GetWordsByLength(count);
selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
}
}
else {
selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
}
}
else {
selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
}
if (selectedWord != null)
{
selectedWord.Used = true;
}
return selectedWord;
}
Next, we need to find all words that can be derived from the game word.
public void FindPossibleWords(AWord selectedWord)
{
this.Clear();
if (selectedWord != null)
{
var possibleWords = (from item in Globals.MainDictionary
where Globals.Contains(selectedWord.Text, item.Text)
select item).ToList<AWord>();
foreach(AWord word in possibleWords)
{
DisplayWord displayWord = new DisplayWord(word, (word.Text == selectedWord.Text));
if (displayWord.IsOriginalWord)
{
this.GameWord = displayWord;
}
this.Add(displayWord);
Debug.WriteLine("{0} words", this.Count);
}
}
}
Finally, we start the game.
public void ResetGame()
{
this.WordCount = this.Count;
this.Statistics.Reset();
InitTimer();
StartTimer();
this.IsPlaying = true;
}
Once game play has started, this object handles the housekeeping when a word is submitted, or when the game is stopped. When a word is submitted, validity must be established, and points awarded (or deducted).
public bool ValidAndScoreWord(string text)
{
text = text.ToUpper();
int points = 0;
bool valid = (!string.IsNullOrEmpty(text));
DisplayWord foundWord = null;
if (valid)
{
foundWord = (from item in this
where (item.Text == text && !item.Found)
select item).FirstOrDefault();
valid = (foundWord != null);
}
if (valid)
{
foundWord.Found = true;
foundWord.SetFoundColor();
points += foundWord.Points;
if (foundWord.IsOriginalWord)
{
points += ORIGINAL_WORD_BONUS;
}
else
{
if (foundWord.Text.Length == GameWord.Text.Length)
{
points += ALL_LETTERS_BONUS;
}
}
this.Statistics.Update(foundWord.Text.Length, points);
if (Settings.TimerMode != TimerMode.NoTimer)
{
int wordRemainder;
Math.DivRem(this.Statistics.WordCount.WordsFound, this.Settings.BonusWords, out wordRemainder);
if (wordRemainder == 0)
{
this.SecondsRemaining += this.Settings.BonusTime;
}
if (foundWord.IsOriginalWord)
{
this.SecondsRemaining += 60;
}
else
{
if (foundWord.Text.Length == GameWord.Text.Length)
{
this.SecondsRemaining += 30;
}
}
this.SecondsAtStart = this.SecondsRemaining;
}
}
else
{
points--;
this.Statistics.Update(0, points);
}
if (this.IsWinner)
{
StopTimer();
}
return (foundWord != null);
}
If the timer is used (configurable in the Setup form), the following method handles the ticks:
public void StartTimer()
{
if (this.Settings.TimerMode != TimerMode.NoTimer)
{
m_timer.Tick += new EventHandler
(
delegate(object s, EventArgs a)
{
this.SecondsRemaining--;
this.PercentRemaining = ((double)this.SecondsRemaining /
Math.Max((double)this.SecondsAtStart, 1)) * 100d;
if (Settings.PlayTickSound)
{
m_soundPlayer.Play();
}
if (this.SecondsRemaining == 0)
{
StopTimer();
}
}
);
m_timer.Start();
}
this.IsPlaying = true;
}
All other methods are for starting, stopping, and solving the game
The Views
As with most WPF apps, the most interesting stuff (which also happens to be the stuff that's the biggest pain in the ass for people like me that haven't drank the WPF kool-aid) happens in the XAML. The code-behind is minimal (as it probably should be), with the main window having just a little under 120 lines of code in it (excluding comments).
The Main Window
This is where the game is actually played. There were several relatively challenging aspects of this form.
The ProgressBar
The ProgressBar
is used to show time remaining in the form of both a text display of minutes/seconds remaining, and a graphic representation of the percentage of time remaining. Since the ProgressBar
already showed the graphical stuff, my task was to add the text display, which is illustrated below:
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2">
<Label x:Name="PART_TextDisplay"
Content="{Binding Path=SecondsRemaining, Converter={StaticResource SecondsRemaining}}"
FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
In order to display the time remaining, I created the TimeRemainingConverter
converter. The ProgressBar.Value
property is bound to the GameDictionary.SecondsRemaining
property.
The Game Word
The game word is comprised of two controls overlaying each other. If a game is underway, the game word is displayed. Otherwise, the Game Over control is displayed. I did this to give a more visually alarming indication that the game had expired (for whatever reason.
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,5" x:Name="textboxGameWord"
CharacterCasing="Upper" Text="" IsReadOnly="true" Focusable="False"
removed="Silver" FontWeight="Bold" BorderBrush="Black" />
<Border Grid.Row="1" Grid.Column="1" Focusable="False" removed="Red" BorderBrush="Black"
BorderThickness="1" Margin="0,0,0,5"
Visibility="{Binding Path=IsPlaying, Converter={StaticResource isPlayingConverter}}" >
<TextBlock Focusable="False" removed="Red" Foreground="Yellow" FontWeight="Bold"
FontStyle="Italic" Text="GAME OVER!!" VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</Border>
The most interesting part of that is the use of the IsPlayingVisibilityConverter
.
The User Word
The user word control actually helps the filter mechanism work. As the user types, the list of possible words that have been found are chcked to see if the word starts with the text that's been typed so far. This is done to help the user to avoid submitting duplicate words (which, if done, causes a one point deduction for the game. The code for this is handled in code-behind via the TextChanged
event which then causes the view model to set the SatisfiesFilter
property. This is one of those places that a more WPF-sih approach is available by way of the ICollectionView
, but that I chose to approach in a more contextual way (within the implementation already performed in the GameDictionary
view model object).
The ListBox
I needed the ListBox
to be able to show items in a custom fashion, namely certain items were to be certain colors, and those items would only be shown if the user found them (or when the user clicked the Solve button. So, I simply replaced the ContentPresenter
in the ListBoxItem
style with
the following:
<Grid >
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" Margin="5,0,0,0">
<TextBlock x:Name="PART_Word" Text="{Binding Path=Text}" FontStyle="Italic" />
<TextBlock x:Name="PART_Adorner" Text="**" FontStyle="Italic"
Visibility="{Binding Path=IsOriginalWord, Converter={StaticResource BoolToVisibility}}" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,0,5,0">
<TextBlock x:Name="PART_Points" Text="{Binding Path=Points}" FontStyle="Italic" />
</StackPanel>
</Grid>
In general the item is displayed in italic, but word would be adorned with a doubleasterisk ("**") if it represented toe original game word. I also added the word points to the item.
Next, a given items would only be visible if the word was "found". I used the built-in WPF converter for this
<Setter Property="Visibility" Value="{Binding Path=Found, Converter={StaticResource BoolToVisibility}}" />
Finally, the item's foreground color would depend on the status of the word. If the word is not found, it would be gray (after the puzzle was solved), blue if it was found, or red if it was found and was also the original game word.
<Setter Property="Foreground" Value="{Binding Path=Foreground}" />
The End-of-Game Statisics GroupBox
Most of you should be aware of this by now, but the standard WPF GroupBox
control is (IMHO) designed incorrectly. The border of the groupbox, when placed over a container whose background is not transparent, displays a non-transparent inner and outer border. Since I'm using a Light Steel Blue background, it was painfully evident. This is what it looked like:
In order to correct it, I had to edit the standard template to change these borders to transparent.
<Border BorderBrush="Transparent" BorderThickness="{TemplateBinding BorderThickness}"
removed="{TemplateBinding Background}" Grid.ColumnSpan="4" Grid.Column="0"
CornerRadius="4" Grid.Row="1" Grid.RowSpan="3"/>
<Border x:Name="Header" Grid.Column="1" Padding="3,1,3,0" Grid.Row="0" Grid.RowSpan="2">
<ContentPresenter ContentSource="Header" RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ContentPresenter Grid.ColumnSpan="2" Grid.Column="1" Margin="{TemplateBinding Padding}"
Grid.Row="2" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
<Border BorderBrush="Transparent"
BorderThickness="{TemplateBinding BorderThickness}"
Grid.ColumnSpan="4" CornerRadius="4" Grid.Row="1" Grid.RowSpan="3">
<Border.OpacityMask>
<MultiBinding ConverterParameter="7"
Converter="{StaticResource BorderGapMaskConverter}">
<Binding ElementName="Header" Path="ActualWidth"/>
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
<Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</Border.OpacityMask>
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3">
<Border BorderBrush="Transparent"
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2"/>
</Border>
</Border>
</Border>
Problem solved.
The Statistics Themselves
The count of words found of given sizes required the use of a ConverterParameter
(and is the reason behind the existince of the WordCounts
/WordCountItem
classes. I needed to be able to
specify the size of the words to retrieve the count for, but you can only have one converter per binding. Happily, I knew the desired word size, so I was able to use the ConverterParameter
to help the converter do "the right thing".
<TextBlock Grid.Column="1" Grid.Row="2">
<TextBlock.Text>
<Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}"
ConverterParameter="3" />
</TextBlock.Text>
</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="0,0,0,5" >
<TextBlock.Text>
<Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}"
ConverterParameter="4" />
</TextBlock.Text>
</TextBlock>
...
...
<TextBlock Grid.Column="1" Grid.Row="3" Margin="0,0,0,5" >
<TextBlock.Text>
<Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}"
ConverterParameter="10" />
</TextBlock.Text>
</TextBlock>
Doing it this way solves the immediate problem, but is not sufficient for the design of the model itself. the model is adaptive where the largest number of letters is concerned, and this design does NOT take that into consideration. The view model itself does (there is one word cound item from 3 to "longest word size"), so one day, I might address this appropriately.
The Setup Window
This view allows the user to changing settings in order to alter gameplay. The only problem I encountered here was with radio buttons due to their nature of by checking one, another is unchecked. I didn't feel like resolving the issue, so all RadioButton
setting/getting is performed in the code behind. All of the "solutions" I saw on the internet were simply hacks around the issue, and I didn't feel like messing with it.
The Winner Window
In the event that the user actually wins a game (finding all words within the current game word), this window is displayed. I don't have a screen shot of this because I've never actually won a game. " src="http://www.codeproject.com/script/Forums/Images/smiley_smile.gif" />
Other Aspects of the Code
The Globals Object
This object is a convenient container for stuff that needs to be accessible duiring the entire application session.
The Contains Method
When a game is started, this method is called for all words in the main dictionary that are smaller than or equal to the size of the game word. I do this when the game starts to avoid any delays during actual game play.
First I created a sorted list of characters for the container (the game word), and the text (the possible word). I realize that it's technically unnecessary to sort the characters, but in reality, it sames a small amount of time in the do/while loop because the loop can potentially break out a little sooner. Needless optimization? Maybe.
public static bool Contains(string container, string inText)
{
bool contains = false;
List<char> containerList = new List<char>();
containerList.AddRange(container);
containerList.Sort();
List<char> testList = new List<char>();
testList.AddRange(inText);
testList.Sort();
Once I have the two lists, I can iterate through each one, testing the current character, and if found, deleting it from each list. If the test list is empty at the end of the iteration, the word is a valid possible word so we return true
.
bool found = false;
do
{
found = false;
for (int i = 0; i < containerList.Count; i++)
{
if (testList[0] == containerList[i])
{
testList.RemoveAt(0);
containerList.RemoveAt(i);
found = true;
break;
}
}
} while (found && testList.Count > 0);
contains = (testList.Count == 0);
return contains;
}
Final Note - this method could probably have gone into the GameDictionary
class, but I was simply too damn lazy to put it there.
The CalcWordScore Method
This method calculates the number of points the specified string is worth (in a game of Scrabble). It utilizes a static array of points and can determine the score regardless of the case of the latter
public static int CalcWordScore(string text)
{
text = text.ToUpper();
int points = 0;
foreach(char character in text)
{
int charInt = Convert.ToInt16(character);
points += m_wordPoints[charInt - ((charInt >= 97) ? 97 : 65)];
}
return points;
}
Final Note - this method could probably have gone into the AWord
class, but I was simply too damn lazy to put it there. Are you starting to see a pattern here?
The RandomNumber Method
Since words (and sometimes word sizes) are selected at random, I needed to use the .Net Random
class. In order for it to be more likely that a more random number is generated each time, I had to instantiate the Random
object whenever I needed to use it. So, I created this method to do the dirty
work.
public static int RandomNumber(int min, int max)
{
return new Random().Next(min, max);
}
Again, this method could have gone into the GameDictionary
class, but blah, blah, blah....
The IntToEnum Method
I came up with this method several years ago to protect my code from exceptions caused by manually edited data and its potential for causing problems. I even wrote a tip about it - Setting Enumerators From Questionable Data Sources (for C# and VB)[^]. If you're interest in what it does, you can go read the tip.
How to Play the Game
Game play is quite simple. Simply start the app. The foirst thing you'll see is this message box:
If everything is okay, you'll be told to "gird your loins". If it can't find the dictionary file, you will be informed, and the game will not proceed beyond this point. (The dictionary file has to be in the application folder.)
Assuming your own personal electronic world hasn't fallen into anarchy (the dictionary file was found), click okay, and you'll see this:
Click the New Game button to start a game. At that point, a randomly selected word will be presented in its scrambled form, and you can immediately start typing in the User Word field.
Just type a word, and press the Return key. If the word is valid, it
will be displayed in the list along with the points earned for that word (not counting any bonus points that also may have been earned). As you enter words, the area at the top/right part of the window will show you how many words are possible, how many you've found, and how many points you've earned. Beneath the New Game and Solve buttons, you'll see a few statistics that are also updated as you play.
When you've found all of the words you can, click the Solve button, and the list
will be updated to show ALL of the words that were possible. Here's what the form will look like when you hit the solve button. Notice the last word in the list that's followed by two asterisks - this is the original word that was scrambled for the game. Ironically, the word turned out to be COMPILING.
And here's and example of some words that were found during the game.
Some details:
- The words you found will be displayed in blue.
- The words you did NOT find will be displayed in gray.
- If you found the original word, it will be displayed in red. It is also displayed with two asterisks (whether you found it or not) to indicate that it's the word that was originally scrambled for the current game.
If you're playing a timed game, the timer at the top of the window will count down to zero, and at that point the game is over. Bonus time can be configured in the Setup window, and can be awarded every time the player finds a specified number of words.
Final Comments
As I said at the top of this article, this is a simple game. The original version was my first solely owned/designed .Net app, and was written with Windows Forms. This version contains a lot less code, and a simpler model/view model from which to work.
After playing the game a bit, I've decided that it really needs some sort of audible indication that the game has expired. Maybe one day...
Changes - 18 Oct 2012
While playing the game, I was annoyed by some of the things I was encountering. Granted, these are very minor things, but they annoyed me none the less.
What Word Did I Just Submit?
Some of the scrambled words allow a large number of words to be found, and when the listbox starts to fill with words, you start to wonder if the word you typed was accepted. The problem is that it takes time for you to scroll the list to find it (and that timer is ticking down while you're looking for the word - tick, tock).
This change had a moderate impact on the view model and the XAML. First, I had to add a property to the DisplayWord
class to indicate that the word was the last one found
:
public bool IsLastWordFound
{
get { return m_isLastWordFound; }
set
{
m_isLastWordFound = value;
RaisePropertyChanged("IsLastWordFound");
}
}
Next, I added a new property to the GameDictionary
class to retain a reference to the last word found. This is just so I don't have to search the list every time a word is submitted to find the word that's marked as the last word found. As you can see, setting the last word found also unsets the last word status on the previously found last word:
public DisplayWord LastWordFound
{
get { return m_lastWordFound; }
set
{
if (m_lastWordFound != null)
{
m_lastWordFound.IsLastWordFound = false;
}
m_lastWordFound = value;
if (m_lastWordFound != null)
{
m_lastWordFound.IsLastWordFound = true;
}
}
}
Finally, I added the a pair of Path
elements to serve as last-word-found indicators in the ListBoxItem
style in the XAML.
<Path Stretch="Uniform" Stroke="DarkGreen" Fill="DarkGreen" Data="M 0,0 7.5,7.5 0,15 0,0" Grid.Column="0"
Visibility="{Binding Path=IsLastWordFound, Converter={StaticResource BoolToVisibility}}" />
<StackPanel Grid.Column="1" HorizontalAlignment="Left" Orientation="Horizontal" Margin="5,0,0,0">
<TextBlock x:Name="PART_Word" Text="{Binding Path=Text}" FontStyle="Italic" />
<TextBlock x:Name="PART_Adorner" Text="**" FontStyle="Italic" Visibility="{Binding Path=IsOriginalWord,
Converter={StaticResource BoolToVisibility}}" />
</StackPanel>
<TextBlock Grid.Column="2" x:Name="PART_Points" Text="{Binding Path=Points}" FontStyle="Italic" Margin="0,0,5,0" />
<Path Stretch="Uniform" Stroke="DarkGreen" Fill="DarkGreen" Data="M 15,0 15,15 7.5,7.5 15,0" Grid.Column="3"
Visibility="{Binding Path=IsLastWordFound, Converter={StaticResource BoolToVisibility}}" />
The final part of this feature change is the act of scrolling the last-found-word into view. I simply added the following line to the indicated method:
private void buttonSubmit_Click(object sender, RoutedEventArgs e)
{
...
...
this.wordList.ScrollIntoView(CurrentGameDictionary.LastWordFound);
}
Game Word Background Color
I sometimes play without the timer, and this makes the background color of the to similar to the color of the ProgressBar
control just above the Game Word. So, I changed the background color of the Game Word control to be LightSteelBlue
Game Word Double-Spacing
I was having a small problem with determining the letters available to me in a given game, so I decided i wanted to see about changing the kerning of the string. If you were hoping for a definitive way to do this, I'm sorry to disappoint you, but I wimped out and simply wrote a new converter class (see the DoubleSpaceConverter
in the source code) that puts a space between the letters.
New Game Button
During game play, I was accidentally clicking the New game button when I actually wanted to click the Solve button first. To solve this problem, I added the following line to the indicated method in MainWindow.Xaml.cs:
private void UpdateButtons()
{
this.buttonNewGame.IsEnabled = ((CurrentGameDictionary == null) || (CurrentGameDictionary != null && !CurrentGameDictionary.IsPlaying));
...
...
}
Final Note
The screen shot provided in this section also illustrates the 1 / NN thing I did a few days ago.
04 Nov 2012 - Fixes and Features
Most programmers simply can't leave well enough alone, and that goes for me, too. I play this game a lot, and I wanted to see some cumulative statistics. I also wanted to fix the scroll bar issue.
Fixes
New Statistics Feature
Since I play the game a lot, I wanted a way to save cumulative statistics so I can see how poorly I do in the game over time. Since there were quite a few metrics to retain, I decided to change the Statistics display to a TabControl
. The Current game tab shows the same stats we had before, and the Cumulative tab shows the - well - the cumulative stats. Here are some screen shots to give you an idea.
Lastly, some changes were made to program flow and functionality to accommodate the new statistics.
- When you start a new game, the tab control automatically switches the Current Game page.
- When you win or solve a running game, or if time expires on a running game, the tab control automatically select the Cumulative page.
- Cumulative statistics can be reset either from the Cumulative tab, or from the Setup window.
- Cumulative statistics are tuned on by default, but can be toggled on/off in the Setup window.
The other major change was the inclusion of .Net class names into the dictionary. I wrote a WinForms app that references all of the .Net 4.5 assemblies, and extracts all of class names from them. Then, it removes non-numeric characters from these discovered class names, and creates a text file that contains any class name that is 3-10 characters long. The file is created in the appropriate folder, and is loaded by default by the Anangrams2 application. There are a few hundred class names that are already covered by the standard scrabble dictionary, so the class name version of the word is not included in the resulting overall dictionary. The act of loading this additional file increases the initialization time for the Anagrams2 app by a few seconds because of the filtering being done. If you're not interested in loading the extra file, simply delete it. In the Setup window, you can elect to include class names in your games.
The interesting bit of the code involved in discovering the class names is as follows:
private void BuildWordListFromClassNames()
{
Assembly[] assemblies = null;
try
{
assemblies = (from assembly in AppDomain.CurrentDomain.GetAssemblies().AsParallel()
select assembly).ToArray<Assembly>();
}
catch (Exception)
{
}
try
{
if (assemblies != null)
{
foreach (Assembly assembly in assemblies)
{
var types = (from type in assembly.GetTypes().AsParallel()
where CheckWordlength(MassageName(type.Name.ToUpper()).Length)
select MassageName(type.Name.ToUpper())).ToArray<string>();
if (types.Length > 0)
{
foreach(string word in types)
{
if (!classnames.Contains(word))
{
classnames.Add(word);
}
}
}
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Oops!");
}
classnames.Sort();
this.label2.Text = classnames.Count.ToString();
this.listBox1.Items.AddRange(classnames.ToArray());
}
History
- 14 Oct 2012 - Original version
- 15 Oct 2012 - While writing the article, I commented out the
GroupBox
style used to eliminate the non-transparent border borders so I could get a screenshot of it. I was playing the game this morning when I noticed that it was still commented out. I uncommented the xaml, recompiled, and uploaded the source again.
- 15 Oct 2012 - Change #2 - I added the number of possible words for each letter count in the Game Statistics group box. This required a change to a couple the
GameStatistics
, WordCounts
, and WordCountItem
constructors, as well as the converter that processes the data. the result is that the info is shown like this - 3-letter words: 0 / NN
, where NN is the possible number of 3-letter words. I also changed the ListBox font to Consolas. The ZIP file has been re-uploaded - again.
- 15 Oct 2012 - IMPORTANT NOTICE! *I THINK* you have to install .Net 4.5 before running this application, or change the set method on the
GameDictionary.PercentRemaining
property to public
by removing the private
accessor).
- 18 Oct 2012 - Added some usability changes (detailed above).
- 04 Nov 2012 - Added a couple of features and fixed a couple of bugs (detailed above).